package snafu.framework.actions;

import java.util.ArrayList;
import java.util.List;

import snafu.framework.agents.Agent;
import snafu.framework.agents.AgentState;
import snafu.framework.objects.WorldObject;
import snafu.framework.worlds.WorldMap;
import snafu.implementation.actions.kill.KillAction;
import snafu.implementation.states.literals.have.HaveLiteral;

/**
 * An class representing an abstract action. All actions have to be sub classes of this class.
 */
public abstract class AbstractAction {
	/** The agent initiating the action. */
	protected Agent agent;
	/** The targets of the action. */
	protected List<WorldObject> targets;
	/** The intention that created the action. */
	protected Intention<?> intention;

	/**
	 * Create an action with a list of targets and the intention that created the action.
	 * 
	 * @param targets
	 *            the list of targets, may be empty
	 * @param intention
	 *            the intention
	 */
	public AbstractAction(List<WorldObject> targets, Intention<?> intention) {
		addTargets(targets);
		this.intention = intention;
	}
	
	
	/**
	 * Set the initiator of the action to the given agent.
	 * 
	 * @param agent
	 *            the agent
	 */
	public void setAgent(Agent agent) {
		this.agent = agent;
	}
	
	/**
	 * Returns the initiator of the action.
	 * 
	 * @return the agent that wants to execute this action
	 */
	public Agent getAgent() {
		return this.agent;
	}
	
	/**
	 * Adds the object to the list of targets.
	 * 
	 * @param target
	 *            the target to add
	 */
	public void addTarget(WorldObject target) {
		getTargets().add(target);
	}
	
	/**
	 * Adds all given objects to the list of targets.
	 * 
	 * @param targets
	 *            the targets to add
	 */
	public void addTargets(List<WorldObject> targets) {
		getTargets().addAll(targets);
	}
	
	/**
	 * Returns the list of targets. Creates an empty list of the targets are null
	 * 
	 * @return the targets, may be empty
	 */
	public List<WorldObject> getTargets() {
		if (this.targets == null) {
			this.targets = new ArrayList<WorldObject>();
		}
		
		return this.targets;
	}

	/**
	 * Indicates whether the action is applicable given the current state of the world. Will use the
	 * method {@link Intention#isApplicable(WorldMap, AgentState, List<WorldObject>)} of the
	 * intention attached to this action to determine this value.
	 * 
	 * @param miniMap
	 *            the world the action should be applied to
	 * @return true of the action is applicable through the {@code isApplicable()} method from the
	 *         intention, false otherwise or if there's more than one target and the action doesn't
	 *         support multiple targets
	 */
	public boolean isApplicable(WorldMap miniMap) {
		// more targets than allowed
		if (targets.size() > 1 && !supportMultiTargets()) {
			return false;
		}

		// ask the intention
		return intention.isApplicable(miniMap, (AgentState) agent.getState(), targets);
	}

	/**
	 * Replaces all objects in the target list with the objects found on the map. If not in the map
	 * the agents HaveLiterals will be checked. Then executes the action using the method
	 * {@link #execute(WorldMap)}.
	 * 
	 * @param worldMap
	 *            the world map
	 */
	public void prepareAndExecute(WorldMap worldMap) {
		
		if (KillAction.class.isAssignableFrom(this.getClass())) {
			System.out.println("Killer!");
		}
		
		// replace target objects, don't touch new objects
		for (int i = 0; i < targets.size(); i++) {
			WorldObject target = targets.get(i);
			
			// try to get real object from map
			WorldObject mapTarget = worldMap.getObject(target.getId());
			if (mapTarget != null) {
				targets.set(i, mapTarget);
				continue;
			}

			// try to get real object from state
			List<HaveLiteral> literals = agent.getState().getLiterals(HaveLiteral.class);
			for (HaveLiteral literal : literals) {
				if (target.equals(literal.getValue())) {
					targets.set(i, literal.getValue());
					continue;
				}
			}
		}
		
		// execute the action
		execute(worldMap);

	}
	
	/**
	 * Executes the action in the given world.
	 * 
	 * @param worldMap
	 *            the world
	 */
	public abstract void execute(WorldMap worldMap);
	
	/**
	 * Returns the string representation of the action.
	 * 
	 * @return the string representation
	 */
	public abstract String output();

	/**
	 * Indicates whether the action support multiple targets.
	 * 
	 * @return true if the action supports multiple targets
	 */
	public abstract boolean supportMultiTargets();
	
	@Override
	public String toString() {
		return output();
	}
}
